/**
 * 
 */
package org.rollinitiative.d20.entity;

import javax.xml.namespace.QName;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rollinitiative.d20.AdjustableValue;
import org.rollinitiative.d20.AdjustableValueListener;
import org.rollinitiative.d20.Adjustment;
import org.rollinitiative.d20.AdjustmentListener;
import org.rollinitiative.d20.ResourceException;
import org.rollinitiative.d20.ResourcePool;

/**
 * The HitPoints class manages the hit points for an entity. Hit points are managed as a
 * ResourcePool with support for an optional reserve. In general, Max hitPoints = base hp + (level *
 * conModifer).
 * 
 * @author bebopjmm
 * 
 */
public class HitPoints implements AdjustableValueListener
{
   static final Log LOG = LogFactory.getLog(HitPoints.class);

   public enum LifeState {
      ALIVE, DYING, DEAD;

      public static LifeState assess(int poolCurrent, int deathThreshold)
      {
         if (poolCurrent > 0)
            return ALIVE;
         else if (poolCurrent <= deathThreshold)
            return DEAD;
         else
            return DYING;
      }
   };

   private ResourcePool hp;

   private ResourcePool rp;

   private ConstitutionAdjustment abilityBonus;

   private AdjustableValue maxHP;

   private int level = 0;

   private int deathThreshold = 0;

   boolean hasReserve = false;


   /**
    * Constructs a new HitPoints object with the designated number of hit points and an optional
    * reserve.
    * 
    * @param hitPoints number of hit points
    * @param hasReserve true if a reserve is to be established
    */
   public HitPoints(int hitPoints, boolean hasReserve)
   {
      maxHP = new AdjustableValue(hitPoints);
      maxHP.setName("maxHP");
      maxHP.subscribe(this);
      hp = new ResourcePool(hitPoints, true);
      hp.setDeficitAllowed(true);
      hp.setSurplusAllowed(true);
      this.hasReserve = hasReserve;
      if (this.hasReserve) {
         rp = new ResourcePool(hitPoints, true);
      }
      else {
         rp = new ResourcePool(0, true);
      }
   }


   /**
    * Constructs a new HitPoints object with 0 hit points. The Constitution AbilityValue is linked
    * to properly modify the hit points based on Constitution value and level. An optional reserve
    * is supported.
    * 
    * @param conVal Constitution AbilityValue to influence hit points
    * @param hasReserve true if a reserve is to be established
    */
   public HitPoints(AbilityValue conVal, boolean hasReserve)
   {

      hp = new ResourcePool(0, true);
      hp.setDeficitAllowed(true);
      hp.setSurplusAllowed(true);
      rp = new ResourcePool(0, true);
      abilityBonus = new ConstitutionAdjustment(conVal);
      maxHP = new AdjustableValue(0);
      maxHP.setName("maxHP");
      maxHP.subscribe(this);
      maxHP.addAdjustment(abilityBonus);
      this.hasReserve = hasReserve;
   }


   /**
    * This method increased the hit point pool by increaseHP, and increments the level value used in
    * calculating the adjustment due to Constitution modifier.
    * 
    * @param increaseHP number of hit points to be added due to level advancement
    */
   public void advanceLevel(int increaseHP)
   {
      LOG.info("Advancing Level: increaseHP = " + increaseHP);
      level++;
      int oldBaseHP = maxHP.getBase();
      maxHP.setBase(oldBaseHP + increaseHP);
      abilityBonus.setLevel(level);
   }


   /**
    * @param hasReserve
    */
   public void setHasReserve(boolean hasReserve)
   {
      if (this.hasReserve == hasReserve) {
         return;
      }

      this.hasReserve = hasReserve;
      if (this.hasReserve) {
         // TODO fill rp to match current hp

      }
      else {
         // TODO empty rp
      }
   }


   /**
    * This method returns true if a reserve is in place, otherwise false.
    * 
    * @return true if a reserve is in place, otherwise false.
    */
   public boolean hasReserve()
   {
      return this.hasReserve;
   }


   /**
    * This method returns the current number of hit points.
    * 
    * @return current number of hit points.
    */
   public int getCurrentHP()
   {
      return hp.getPoolCurrent();
   }


   /**
    * This method returns the maximum number of hit points.
    * 
    * @return maximum number of hit points
    */
   public int getMaxHP()
   {
      return hp.getPoolMax();
   }


   /**
    * This method returns the current number of reserve hit points.
    * 
    * @return current number of reserve hit points.
    */
   public int getReserveHP()
   {
      return rp.getPoolCurrent();
   }


   /**
    * @return the deathThreshold
    */
   public int getDeathThreshold()
   {
      return deathThreshold;
   }


   /**
    * @param deathThreshold the deathThreshold to set
    */
   public void setDeathThreshold(int deathThreshold)
   {
      if (deathThreshold > 0) {
         LOG.warn("Ignoring attempt to set death threshold > 0");
         return;
      }
      this.deathThreshold = deathThreshold;
   }


   /**
    * This method inflicts the designated amount of hit point damage to the actor.
    * 
    * @param hp amount of damage to inflict
    * @return LifeState following the damage.
    */
   public LifeState damage(int hp)
   {
      synchronized (this.hp) {
         try {
            this.hp.expend(hp);
            return LifeState.assess(this.hp.getPoolCurrent(), this.deathThreshold);
         } catch (ResourceException ex) {
            return LifeState.DEAD;
         }
      }
   }


   /**
    * This method cures the designated amount of hit points to the actor. A DEAD actor cannot
    * receive healing.
    * 
    * @param hp amount of curing to apply
    * @return LifeState following the curing
    */
   public LifeState heal(int hp)
   {
      if (LifeState.assess(this.hp.getPoolCurrent(), deathThreshold) == LifeState.DEAD) {
         LOG.info("Cannot heal a dead creature");
         return LifeState.DEAD;
      }
      synchronized (this.hp) {
         this.hp.replenish(hp);
         return LifeState.assess(this.hp.getPoolCurrent(), this.deathThreshold);
      }
   }


   /**
    * This method is triggered whenever maxHP is altered. It triggers the recalculation of pool
    * levels.
    * 
    * @seeorg.rollinitiative.d20.AdjustableValueListener#valueChanged(org.rollinitiative.d20. 
    *                                                                                         AdjustableValue
    *                                                                                         )
    */

   @Override
   public void valueChanged(AdjustableValue adjustable)
   {
      if (LOG.isDebugEnabled()) {
         LOG.debug("Recalculating hpPool due to change in maxHP.");
      }
      try {
         recalcMax();
      } catch (ResourceException ex) {
         LOG.error("Failure to update the hp/rp pools!", ex);
      }
   }


   /**
    * This method updates the max pool sizes of hp and rp based on changes in the maxHP
    * AdjustableValue.
    */
   protected void recalcMax() throws ResourceException
   {
      if (LOG.isDebugEnabled()) {
         LOG.debug("Assessing maxHP: New max=" + maxHP.getCurrent() + ", old max="
               + hp.getPoolMax());
      }
      int delta = maxHP.getCurrent() - hp.getPoolMax();
      hp.updateMax(delta, true);
      if (hasReserve) {
         rp.updateMax(delta, true);
      }
   }

   /**
    * The ConstitutionAdjustment calculates the hp adjustment due to level and CON ability modifier
    * where adjustment = level * abilityMod
    * 
    * @author bebopjmm
    * 
    */
   class ConstitutionAdjustment extends Adjustment implements AdjustmentListener
   {
      Adjustment conAdj_;


      public ConstitutionAdjustment(AbilityValue conVal)
      {
         super(AdjustmentSource.INHERENT, 0, new QName("java:org.rollinitiative.d20.entity", "hitPoints.CON"));
         conAdj_ = conVal.getModifier();
         conAdj_.subscribe(this);
         this.setValue(level * conAdj_.getValue());
      }


      /**
       * This method assigns the value of the adjustment based on newLevel and the current CON
       * adjustment. It should trigger the valueChanged() method on subscribed maxHP
       * AdjustableValues.
       * 
       * @param newLevel total levels for CON adjustment.
       */
      public void setLevel(int newLevel)
      {
         this.setValue(newLevel * conAdj_.getValue());
      }


      /**
       * This will be invoked when the CON modifier changes.
       * 
       * @see org.rollinitiative.d20.AdjustmentListener#valueChanged(org.rollinitiative.d20.Adjustment)
       */
      @Override
      public void valueChanged(Adjustment adjustment)
      {
         LOG.debug("Updating CON adjustment to hp = level(" + level + ") * modifier("
               + adjustment.getValue() + ")");
         this.setValue(level * adjustment.getValue());
      }
   }
}
